package cjakma.study.portfolio.common.validation;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.metadata.ConstraintDescriptor;

import org.springframework.beans.NotReadablePropertyException;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.core.Conventions;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;

/**
 * Validation 유틸리티 클래스
 * 
 * @author 서성수
 */
public abstract class ValidationUtils extends org.springframework.validation.ValidationUtils {

	private static final Set<String> internalAnnotationAttributes = new HashSet<String>(3);
	static {
		internalAnnotationAttributes.add("message");
		internalAnnotationAttributes.add("groups");
		internalAnnotationAttributes.add("payload");
	}

	/**
	 * Validate.
	 * 
	 * @param validator
	 *            the {@link Validator}
	 * @param target
	 *            the target object
	 * @param groups
	 *            group or list of groups targeted for validation (default to {@link javax.validation.groups.Default})
	 * @return {@link BindingResult}
	 */
	public static BindingResult validate(Validator validator, Object target, Class<?>... groups) {
		return validate(validator, target, (String) null, groups);
	}

	/**
	 * Validate.
	 * 
	 * @param validator
	 *            the {@link Validator}
	 * @param target
	 *            the target object
	 * @param objectName
	 *            the name of the target object
	 * @param groups
	 *            group or list of groups targeted for validation (default to {@link javax.validation.groups.Default})
	 * @return {@link BindingResult}
	 */
	public static BindingResult validate(Validator validator, Object target, String objectName, Class<?>... groups) {
		Assert.notNull(validator, "Validator must not be null");
		Assert.notNull(target, "Target must not be null");
		if (objectName == null) {
			objectName = Conventions.getVariableName(target);
		}
		// Create data binder and validate
		DataBinder binder = new DataBinder(target, objectName);
		BindingResult bindingResult = binder.getBindingResult();
		validate(validator, target, bindingResult, groups);
		return bindingResult;
	}

	/**
	 * Validate.
	 * 
	 * @param validator
	 *            the {@link Validator}
	 * @param target
	 *            the target object
	 * @param errors
	 *            the {@link Errors}
	 * @param groups
	 *            group or list of groups targeted for validation (default to {@link javax.validation.groups.Default})
	 */
	public static void validate(Validator validator, Object target, Errors errors, Class<?>... groups) {
		Assert.notNull(validator, "Validator must not be null");
		Assert.notNull(errors, "Errors must not be null");
		Set<ConstraintViolation<Object>> result = validator.validate(target, groups);
		for (ConstraintViolation<Object> violation : result) {
			String field = violation.getPropertyPath().toString();
			FieldError fieldError = errors.getFieldError(field);
			if (fieldError == null || !fieldError.isBindingFailure()) {
				try {
					errors.rejectValue(field, violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName(), getArgumentsForConstraint(errors.getObjectName(), field, violation.getConstraintDescriptor()), violation.getMessage());
				} catch (NotReadablePropertyException ex) {
					throw new IllegalStateException(
							"JSR-303 validated property '" + field + "' does not have a corresponding accessor for Spring data binding - " + "check your DataBinder's configuration (bean property versus direct field access)",
							ex);
				}
			}
		}
	}

	/**
	 * Return FieldError arguments for a validation error on the given field. Invoked for each violated constraint.
	 * <p>
	 * The default implementation returns a first argument indicating the field name (of type DefaultMessageSourceResolvable, with "objectName.field" and
	 * "field" as codes). Afterwards, it adds all actual constraint annotation attributes (i.e. excluding "message", "groups" and "payload") in alphabetical
	 * order of their attribute names.
	 * <p>
	 * Can be overridden to e.g. add further attributes from the constraint descriptor.
	 * 
	 * @param objectName
	 *            the name of the target object
	 * @param field
	 *            the field that caused the binding error
	 * @param descriptor
	 *            the JSR-303 constraint descriptor
	 * @return the Object array that represents the FieldError arguments
	 * @see org.springframework.validation.FieldError#getArguments
	 * @see org.springframework.context.support.DefaultMessageSourceResolvable
	 * @see org.springframework.validation.DefaultBindingErrorProcessor#getArgumentsForBindError
	 */
	protected static Object[] getArgumentsForConstraint(String objectName, String field, ConstraintDescriptor<?> descriptor) {
		List<Object> arguments = new LinkedList<Object>();
		String[] codes = new String[] { objectName + Errors.NESTED_PATH_SEPARATOR + field, field };
		arguments.add(new DefaultMessageSourceResolvable(codes, field));
		// Using a TreeMap for alphabetical ordering of attribute names
		Map<String, Object> attributesToExpose = new TreeMap<String, Object>();
		for (Map.Entry<String, Object> entry : descriptor.getAttributes().entrySet()) {
			String attributeName = entry.getKey();
			Object attributeValue = entry.getValue();
			if (!internalAnnotationAttributes.contains(attributeName)) {
				attributesToExpose.put(attributeName, attributeValue);
			}
		}
		arguments.addAll(attributesToExpose.values());
		return arguments.toArray(new Object[arguments.size()]);
	}

}
